package com.frikinet.lifeguard.fotolog;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathFactory;

import org.lobobrowser.html.UserAgentContext;
import org.lobobrowser.html.domimpl.HTMLDivElementImpl;
import org.lobobrowser.html.domimpl.NodeImpl;
import org.lobobrowser.html.parser.DocumentBuilderImpl;
import org.lobobrowser.html.parser.InputSourceImpl;
import org.lobobrowser.html.test.SimpleUserAgentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.frikinet.lifeguard.App;
import com.frikinet.lifeguard.dao.PostDAO;
import com.frikinet.lifeguard.dao.PostImageDAO;
import com.frikinet.lifeguard.model.Comment;
import com.frikinet.lifeguard.model.Post;
import com.frikinet.lifeguard.model.PostImage;
import com.frikinet.lifeguard.model.User;
import com.frikinet.lifeguard.util.HibernateUtil;

public class FotologDataLoader
{
	private static Logger log = LoggerFactory.getLogger(FotologDataLoader.class);
	
	private static Pattern uploadedPattern = Pattern.compile("Photo uploaded at (\\d?\\d):(\\d\\d) (\\w\\w)");
	private static Pattern saidPattern = Pattern.compile("(.*?) said on (.*?)$");
	private static Pattern repliedPattern = Pattern.compile("(.*?) replied to (.*?)on (.*?)$");
	
	private User user;
	
	UserAgentContext uaContext;
	DocumentBuilderImpl builder;
	XPath xpath = XPathFactory.newInstance().newXPath();
	
	private List<FotologLoaderListener> listeners = new ArrayList<FotologLoaderListener>();

	public FotologDataLoader(String user)
	{
		this.user = getUser(user);
		
		uaContext = new LocalUserAgentContext();
		builder = new DocumentBuilderImpl(uaContext);
	}
	
	private User getUser(String name)
	{
		User dev = null;
		for(User user: App.users)
		{
			if(user.getName().equals(name))
			{
				dev = user;
				break;
			}
		}
		if(dev == null)
		{
			dev = new User();
			dev.setName(name);
		}
		return dev;
	}
	
	public void addLoaderListener(FotologLoaderListener listener)
	{
		listeners.add(listener);
	}
	
	protected void fireLoadStarted()
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.loadStarted();
		}
	}
	
	protected void fireYearsCalculated(SortedSet<Integer> years)
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.yearsCalculated(years);
		}
	}
	
	protected void fireReadingMonth(int month, int year)
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.readingMonth(month, year);
		}
	}
	
	protected void firePostFound(Post post)
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.postFound(post);
		}
	}
	
	protected void firePostLookupFinished(List<Post> posts)
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.postLookupFinished(posts);
		}
	}
	
	protected void firePostProcessed(Post post)
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.postProcessed(post);
		}
	}
	
	protected void fireLoadEnded()
	{
		for(FotologLoaderListener listener: listeners)
		{
			listener.loadEnded();
		}
	}
	
	private void checkUserExists() throws FotologException
	{
		try
		{
			URL url = new URL(String.format("http://www.fotolog.com/%s", user.getName()));
			InputStream in = url.openConnection().getInputStream();
			in.close();
		}
		catch(MalformedURLException e)
		{
			throw new FotologException(App.i18n.translate("errors.user.not.exist", user.getName()));
		}
		catch(FileNotFoundException e)
		{
			throw new FotologException(App.i18n.translate("errors.user.not.exist", user.getName()));
		}
		catch(IOException e)
		{
			if(e.getMessage().indexOf("400") > -1)
			{
				throw new FotologException(App.i18n.translate("errors.user.not.exist", user.getName()));
			}
			else
			{
				log.error("Error parsing Fotolog",e);
				throw new FotologUnexpectedException(e);
			}
		}
	}
	
	public User loadData() throws FotologException
	{
		checkUserExists();
		
		user.getPosts().clear();
		fireLoadStarted();
		
		SortedSet<Integer> years = getYears();
		fireYearsCalculated(years);
		
		//now, we get posts
		List<Post> posts = getPosts(years);
		firePostLookupFinished(posts);
		
		for(Post post: posts)
		{
			Post origin = HibernateUtil.get(Post.class, "uid", post.getUid());
			if(origin == null)
			{
				loadPostData(post);
				PostDAO.saveOrUpdate(post);
				user.getPosts().add(post);
			}
			else
			{
				user.getPosts().add(post);
			}
			firePostProcessed(origin==null ? post : origin);
		}
		
		HibernateUtil.saveOrUpdate(user);
		if(!App.users.contains(user))
		{
			App.users.add(user);
		}
		fireLoadEnded();
		return user;
	}
	
	private SortedSet<Integer> getYears()
	{
		try
		{
			SortedSet<Integer> years = new TreeSet<Integer>();
			Document doc = getDocument(String.format("http://www.fotolog.com/%s/archive", user));
			
			Object div = xpath.evaluate("//div[@id='years']", doc, XPathConstants.NODE);
			//now iterate over 'a' elements and get text
			NodeList links = (NodeList)xpath.evaluate("a/text() | span/text()", div, XPathConstants.NODESET);
			for(int i=0; i < links.getLength(); i++)
			{
				years.add(Integer.parseInt(links.item(i).getNodeValue()));
			}
			return years;
		}
		catch(Exception e)
		{
			log.error("Error parsing Fotolog",e);
			throw new FotologUnexpectedException(e);
		}
	}
	
	private List<Post> getPosts(SortedSet<Integer> years)
	{
		try
		{
			List<Post> posts = new ArrayList<Post>();
			SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
			
			for(int year: years)
			{
				for(int month=1; month<=12; month++)
				{
					log.trace(String.format("Loading calendar page for %d/%d", month, year));
					Document doc = getDocument(String.format("http://www.fotolog.com/%s/archive?year=%d&month=%d", user, year, month));
					NodeList days = (NodeList)xpath.evaluate("//div[@class='day']", doc, XPathConstants.NODESET);
					for(int index=0;index < days.getLength();index++)
					{
						Node node = days.item(index);
						String day = xpath.evaluate("h4/text()", node);
						String href[] = xpath.evaluate(".//a[@class='on']/@href", node).split("/");
						String uid = href[href.length-1];
						Post post = new Post();
						post.setUser(user);
						post.setUid(uid);
						try
						{
							post.setDate(sdf.parse(String.format("%s/%d/%d", day, month, year)));
						}
						catch (ParseException e)
						{
							log.error("Error parsing date", e);
						}
						posts.add(post);
						log.trace(String.format("\tInfo loaded for %s/%s/%s", day, month, year));
						firePostFound(post);
					}
					fireReadingMonth(month, year);
				}
			}		
			return posts;
		}
		catch(Exception e)
		{
			log.error("Error parsing Fotolog",e);
			throw new FotologUnexpectedException(e);
		}
	}
	
	private void loadPostData(Post post)
	{
		try
		{
			log.trace(String.format("Getting data for %s...", post.getDate()));
			SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yy hh:mm aa", Locale.US);
			Document doc = getDocument(String.format("http://www.fotolog.com/%s/%s", user, post.getUid()));
			
			//get image
			PostImage image = new PostImage();
			image.setUid(post.getUid());
			String imgSrc = xpath.evaluate("//div[@id='centerCol']/img/@src", doc);
			URL imgUrl = new URL(imgSrc);
			ByteArrayOutputStream output = new ByteArrayOutputStream();
			DataInputStream input = new DataInputStream(imgUrl.openConnection().getInputStream());
			int b = 0;
			while((b = input.read()) > -1)
				output.write(b);
			input.close();
			output.close();
			image.setImage(output.toByteArray());
			PostImageDAO.saveOrUpdate(image);
			
			//get title
			post.setTitle(xpath.evaluate("//div[@id='photo']/h2/text()", doc).trim());
			
			//get text
			HTMLDivElementImpl div = (HTMLDivElementImpl)xpath.evaluate("//div[@id='caption']", doc, XPathConstants.NODE);
			post.setText(getText(div, "div"));
			
			//get time
			HTMLDivElementImpl uploaded = (HTMLDivElementImpl)xpath.evaluate("//div[@class='uploadedBy']", doc, XPathConstants.NODE);
			Matcher uploadedMatcher = uploadedPattern.matcher(getText(uploaded).trim());
			if(uploadedMatcher.find())
			{
				Calendar calendar = Calendar.getInstance();
				calendar.setTime(post.getDate());
				calendar.set(Calendar.MINUTE, Integer.parseInt(uploadedMatcher.group(2)));
				int hour = Integer.parseInt(uploadedMatcher.group(1));
				if(uploadedMatcher.group(3).equals("PM"))
				{
					hour = (hour + 12) % 24;
				}
				calendar.set(Calendar.HOUR_OF_DAY, hour);
				post.setDate(calendar.getTime());
			}
			
			//get comments
			NodeList comments = (NodeList)xpath.evaluate("//div[@class='comment' and not(.//div[@class='privateMark'])]", doc, XPathConstants.NODESET);
			for(int index=0; index<comments.getLength(); index++)
			{
				NodeImpl node = (NodeImpl)comments.item(index);
				Comment comment = new Comment();
				comment.setPost(post);
				comment.setText(getText((NodeImpl)xpath.evaluate("div[@class='cfx']", node, XPathConstants.NODE)));
				
				String byline = getText((NodeImpl)xpath.evaluate("div[@class='byline']", node, XPathConstants.NODE));
				if(Pattern.matches(saidPattern.pattern(), byline))
				{
					Matcher matcher = saidPattern.matcher(byline);
					matcher.find();
					comment.setAuthor(matcher.group(1));
					comment.setDate(sdf.parse(matcher.group(2)));
				}
				else if(Pattern.matches(repliedPattern.pattern(), byline))
				{
					Matcher matcher = repliedPattern.matcher(byline);
					matcher.find();
					comment.setAuthor(matcher.group(1));
					comment.setReplied(matcher.group(2));
					comment.setDate(sdf.parse(matcher.group(3)));
				}
				
				post.getComments().add(comment);
			}
		}
		catch(Exception e)
		{
			log.error("Error parsing Fotolog",e);
			throw new FotologUnexpectedException(e);
		}
	}
	
	private Document getDocument(String urlAddress)
	{
		try
		{
			//Open URL
			URL url = new URL(urlAddress);
			InputStream in = url.openConnection().getInputStream();
			InputSourceImpl input = new InputSourceImpl(in, url.toString(), "UTF-8");
			Document doc = builder.parse(input);
			in.close();
			return doc;
		}
		catch(Exception e)
		{
			log.error("Error parsing Fotolog",e);
			throw new FotologUnexpectedException(e);
		}
	}
	
	private String getText(NodeImpl node, String... exceptions)
	{
		StringBuffer text = new StringBuffer("");
		Set<String> exceptionsSet = new HashSet<String>(Arrays.asList(exceptions));
		Set<String> preserve = new HashSet<String>(Arrays.asList("br","b","i"));
		
		if(node != null)
		{
			for(NodeImpl child: node.getChildrenArray())
			{
				if(child.getNodeType()==Node.TEXT_NODE)
					text.append(" "+child.getNodeValue().trim());
				else if(child.getNodeName().equalsIgnoreCase("br"))
					text.append("<br/>");
				else if(!exceptionsSet.contains(child.getNodeName().toLowerCase()))
				{
					if(preserve.contains(child.getNodeName().toLowerCase()))
						text.append(String.format(" <%1$s>%2$s</%1$s>", child.getNodeName().toLowerCase(), getText(child, exceptions)));
					else
						text.append(" "+getText(child));
				}
			}
		}
		
		return text.toString().trim();
	}
	
	private static class LocalUserAgentContext extends SimpleUserAgentContext
	{
		@Override
		public boolean isScriptingEnabled()
		{
			return false;
		}

		@Override
		public boolean isExternalCSSEnabled()
		{
			return false;
		}

		@Override
		public String getUserAgent() {
			return "Mozilla/4.0 (compatible; MSIE 6.0;) Cobra/Fotolog Parser";
		}
	}
}
